// hex.cpp : Defines the entry point for the application.
//

#include "stdafx.h"
#include "HexLook.h"
#include <string>
#include <array>
#include <Windowsx.h>
#include <Shobjidl.h>
#include "GotoAddressDialog.h"
#include "SelectionDialog.h"
#include <dlgs.h>



// Global Variables:
static const LONGLONG kInvalidAddress = -1;
static const UINT kWmMessageContinueFind = WM_APP + 0;
static const UINT kWmMessageCheckForReadFinish = WM_APP + 1;
static const UINT kWmMessageContinueCopy = WM_APP + 2;

extern void RegisterHyperlinkControlClass(HINSTANCE inInstance);

// Forward declarations of functions included in this code module:
LRESULT CALLBACK	WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK	About(HWND, UINT, WPARAM, LPARAM);

int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPTSTR    lpCmdLine,
                     _In_ int       nCmdShow)
{
	UNREFERENCED_PARAMETER(hPrevInstance);

	HexApplication app;
	int result = FALSE;
	if (app.InitInstance(hInstance))
	{
		app.OpenFile(lpCmdLine, /*displayName*/ nullptr,
			/*strip quotes*/ true);
		app.Show(nCmdShow);
		result = app.Run();
	}
	return result;

	UNREFERENCED_PARAMETER(lpCmdLine);

}


//
//  FUNCTION: WndProc(HWND, UINT, WPARAM, LPARAM)
//
//  PURPOSE:  Processes messages for the main window.
//
//  WM_COMMAND	- process the application menu
//  WM_PAINT	- Paint the main window
//  WM_DESTROY	- post a quit message and return
//
//
LRESULT CALLBACK WndProc(HWND inHWnd, UINT inMessage, WPARAM wParam, LPARAM lParam)
{
	if (auto pThis = reinterpret_cast<HexApplication *>(
		GetWindowLongPtr(inHWnd, GWLP_USERDATA)))
	{
		return pThis->WindowProcedure(inMessage, wParam, lParam);
	}
	if (WM_CREATE == inMessage)
	{
		auto psc = reinterpret_cast<LPCREATESTRUCT>(lParam);
		LONG_PTR pThisAsLongPtr = reinterpret_cast<LONG_PTR>(psc->lpCreateParams);
		SetWindowLongPtr(inHWnd, GWLP_USERDATA, pThisAsLongPtr);
	}
	return DefWindowProc(inHWnd, inMessage, wParam, lParam);
}

static DWORD sTime = 0;

LRESULT HexApplication::WindowProcedure(UINT message, WPARAM wParam, LPARAM lParam)
{
	LRESULT result = 0;

	if (mFindTextDialog.GetWmMessageId() == message)
	{
		auto pFindStruct = (LPFINDREPLACE)lParam;

		// If the FR_DIALOGTERM flag is set, 
		// invalidate the handle that identifies the dialog box. 
		if (pFindStruct->Flags & FR_DIALOGTERM)
		{
			if (!mFileHandler.IsSearchFinished())
			{
				mFileHandler.StopReading();
				mFileHandler.SetSearchFinished();
			}
			mFindTextDialog.NullifyDialogHandle();
			return 0;
		}

		// If the FR_FINDNEXT flag is set, 
		// call the application-defined search routine
		// to search for the requested string. 
		if (pFindStruct->Flags & FR_FINDNEXT)
		{
			if (mFileHandler.SetWhatVector(mFindTextDialog.GetWhat(pFindStruct)))
			{
				CancelCopying();
				PerformFind(pFindStruct->Flags & FR_DOWN);
			}
		}

		return 0;
	}

	switch (message)
	{
	case WM_COMMAND:
		if (!ProcessCommand(LOWORD(wParam)))
		{
			result = ::DefWindowProc(mWindow, message, wParam, lParam);
		}
		break;
	case kWmMessageContinueFind:
		{
			if (mFileHandler.IsSearchFinished())
			{
				break;
			}
			mFileHandler.GotoNextSearchBlock();
			mFileHandler.AcceptCurrentAsyncBlock();
			bool isAtLastBlock = mFileHandler.IsAtLastSearchBlock();
			if (!isAtLastBlock)
			{
				if (!mFileHandler.LaunchAsyncReadingNextBlock())
				{
					::PostMessage(mWindow, kWmMessageContinueFind, 0, 0);
				}
			}
			mFileHandler.ContinueFind();
			if (isAtLastBlock)
			{
				FinishFind();
			}
			else if (mFileHandler.IsSearchFinished())
			{
				mFileHandler.StopReading();
				FinishFind();
			}
		}
	
		break;
	case kWmMessageFindSettingsAreChanged:
		CancelFind();
		break;
	case kWmMessageCheckForReadFinish:
		if (HANDLE hEvent = mFileHandler.GetEventForAsyncReading())
		{
			if (mFileHandler.IsSearchFinished())
			{
				DWORD r = ::WaitForSingleObject(hEvent, 10);
				if (WAIT_TIMEOUT == r)
				{
					::PostMessage(mWindow, kWmMessageCheckForReadFinish, 0, 0);
				}
				else if (WAIT_OBJECT_0 == r)
				{
					::InvalidateRect(mWindow, nullptr, false);
					if (mCopier.IsCopying())
					{
						mFileHandler.AcceptCurrentAsyncBlock();
						::PostMessage(mWindow, kWmMessageContinueCopy, 0, 0);
					}
				}
			}
		}
		break;
	case kWmMessageContinueCopy:
		ContinueCopying();
		break;
	case kWmMessageCancelCopy:
		CancelCopying();
		break;
	case WM_PAINT:
		if (mViewController.RecreateRenderTarget(mWindow))
		{
			RECT dirtyRect;
			::GetUpdateRect(mWindow, &dirtyRect, false);
			mViewController.Draw(mScrollPosition, dirtyRect, mFileHandler);
			::ValidateRect(mWindow, NULL);
		}
		break;
	case WM_SIZE:
		OnSize(LOWORD(lParam), HIWORD(lParam));
		if (mFileHandler.WasSearchInterrupted())
		{
			CancelFind();
		}
		break;
	case WM_VSCROLL:
		OnScroll(LOWORD(wParam), HIWORD(wParam));
		if (mFileHandler.WasSearchInterrupted())
		{
			CancelFind();
		}
		break;
	case WM_KEYDOWN:
		switch (wParam)
		{
		case VK_UP: OnScroll(SB_LINEUP, 0); 	break;
		case VK_DOWN: OnScroll(SB_LINEDOWN, 0);	break;
		case VK_PRIOR: OnScroll(SB_PAGEUP, 0);	break;
		case VK_NEXT: OnScroll(SB_PAGEDOWN, 0);	break;
		case VK_HOME: OnScroll(SB_TOP, 0);		break;
		case VK_END: OnScroll(SB_BOTTOM, 0);	break;
		default: result = ::DefWindowProc(mWindow, message, wParam, lParam);
		}
		if (mFileHandler.WasSearchInterrupted())
		{
			CancelFind();
		}
		break;
	case WM_POINTERWHEEL:
	{
		int delta = GET_WHEEL_DELTA_WPARAM(wParam);
		OnWheel(-delta);
		if (mFileHandler.WasSearchInterrupted())
		{
			CancelFind();
		}
	}
		break;
	case WM_POINTERDOWN:
	{
		// --------- troubleshooting ------
		char buf[100];
		DWORD t = ::GetTickCount();
		DWORD delta = t - sTime;
		sTime = t;
		sprintf_s(buf, "_%d ", t);
		::OutputDebugStringA(buf);
	}

		if (IS_POINTER_INRANGE_WPARAM(wParam) &&
			0 < mFileHandler.GetFileLength())
		{
			POINT mouse = GetLocalMousePosition(lParam);
			auto y = mouse.y + static_cast<LONGLONG>(mScrollPosition);

			// check the state of Shift key...
			SHORT stateOfShift = ::GetKeyState(VK_SHIFT);
			if (0 == (0x8000 & stateOfShift))
			{
				// no shift
				mViewController.ClearSelection();
				mViewController.StartSelection(mouse.x,
					y, mFileHandler);
				::InvalidateRect(mWindow, nullptr, false);
				mViewController.ResetInvalidatedStrip();
			}
			else
			{
				// shift is pressed
				mViewController.ContinueSelection(mouse.x, 
					y, mFileHandler, true);
				InvalidateSelection();
			}

			result = 0;
		}
		else
		{
			result = ::DefWindowProc(mWindow, message, wParam, lParam);
		}
		break;
	case WM_LBUTTONDOWN:
	{
		int x = GET_X_LPARAM(lParam);
		int y = GET_Y_LPARAM(lParam);
		RECT clientRect;
		if (0 <= x && 0 <= y && ::GetClientRect(mWindow, &clientRect) &&
			x < clientRect.right && y < clientRect.bottom &&
			0 < mFileHandler.GetFileLength())
		{
			auto yDoc = y + static_cast<LONGLONG>(mScrollPosition);

			// check the state of Shift key...
			SHORT stateOfShift = ::GetKeyState(VK_SHIFT);
			if (0 == (0x8000 & stateOfShift))
			{
				// no shift
				mViewController.ClearSelection();
				mViewController.StartSelection(x, yDoc, mFileHandler);
				::InvalidateRect(mWindow, nullptr, false);
				mViewController.ResetInvalidatedStrip();
			}
			else
			{
				// shift is pressed
				mViewController.ContinueSelection(x, yDoc, mFileHandler, true);
				InvalidateSelection();
			}
		}
	}
		break;
	case WM_POINTERUPDATE:
		if (IS_POINTER_INRANGE_WPARAM(wParam))
		{
			if (HCURSOR cursor = ::LoadCursor(0, IDC_IBEAM))
			{
				::SetCursor(cursor);
			}
			result = 0;
			if (IS_POINTER_FIRSTBUTTON_WPARAM(wParam))
			{
				POINT mouse = GetLocalMousePosition(lParam);
				if (0 <= mouse.y)
				{
					auto y = mouse.y + static_cast<LONGLONG>(mScrollPosition);
					mViewController.ContinueSelection(mouse.x,
						y, mFileHandler, true);
					InvalidateSelection();
				}
			}
		}
		else
		{
			result = ::DefWindowProc(mWindow, message, wParam, lParam);
		}
		break;
	case WM_INITMENUPOPUP:
		UpdateMenuItemStates(HMENU(wParam));
		break;
	case WM_DESTROY:
		mFindTextDialog.CloseDialog();
		mFileHandler.StopReading();
		PostQuitMessage(0);
		break;
	default:
		result = DefWindowProc(mWindow, message, wParam, lParam);
	}
	return result;
}


bool HexApplication::ProcessCommand(int commandId)
{
	bool result = true;
	switch (commandId)
	{
	case IDM_OPEN:
		CancelCopying();
		CancelFind();
		ProcessOpenCommand();
		mIsRepeatedFind = false;
		break;
	case IDM_GOTO_ADDRESS:
	{
		GotoAddressDialog dialog(mFileHandler.GetFileLength());
		if (dialog.DoModal(mInstance, mWindow))
		{
			LONGLONG address = dialog.GetAddress();

			mScrollPosition = mViewController.GetYPositionForAddress(address);
			RECT clientRect;
			if (::GetClientRect(mWindow, &clientRect))
			{
				auto height = clientRect.bottom - clientRect.top;
				auto topOfLastScroll = ::fmax(mContentHeight - height, 0);
				auto lineHeight = mViewController.GetLineHeight();
				while (mScrollPosition > topOfLastScroll + 0.9 * lineHeight)
				{
					mScrollPosition -= lineHeight;
				}
			}
			::InvalidateRect(mWindow, nullptr, false);

			int scaledPos = int(mScrollPosition * mScaleForScroll);
			::SetScrollPos(mWindow, SB_VERT, scaledPos, /*redraw*/ true);
		}
	}
		break;
	case IDM_FINDHEX:
		mFindTextDialog.ReopenDialog(SearchKind::kBytesFromHexDigits);
		break;
	case IDM_FINDUTF8:
		mFindTextDialog.ReopenDialog(SearchKind::kUtf8);
		break;
	case IDM_FINDUTF16LE:
		mFindTextDialog.ReopenDialog(SearchKind::kUtf16LE);
		break;
	case IDM_FINDUTF16BE:
		mFindTextDialog.ReopenDialog(SearchKind::kUtf16BE);
		break;
	case IDM_SELECTALL:
		if (0 < mFileHandler.GetFileLength())
		{
			mViewController.SelectAll(mFileHandler);
			::InvalidateRect(mWindow, nullptr, false);
			mIsRepeatedFind = false;
		}
		break;
	case IDM_DESELECT:
		mViewController.ClearSelection();
		::InvalidateRect(mWindow, nullptr, false);
		mIsRepeatedFind = false;
		break;
	case IDM_SELECTCUSTOM:
	{
		SelectionDialog dialog(mFileHandler.GetFileLength());
		if (dialog.DoModal(mInstance, mWindow))
		{
			auto address1 = dialog.GetAddress1();
			auto address2 = dialog.GetAddress2();
			mViewController.SetSelection(address1, address2);

			auto addressSmallest = min(address1, address2);
			auto smallestPos = mViewController.GetYPositionForAddress(addressSmallest);
			bool isNeedChangeScrollPosition = false;
			if (smallestPos < mScrollPosition)
			{
				isNeedChangeScrollPosition = true;
			}
			RECT clientRect;
			if (::GetClientRect(mWindow, &clientRect) && 
				smallestPos > mScrollPosition + clientRect.bottom -
				mViewController.GetLineHeight())
			{
				isNeedChangeScrollPosition = true;
			}
			if (isNeedChangeScrollPosition)
			{
				auto height = clientRect.bottom - clientRect.top;
				auto topOfLastScroll = ::fmax(mContentHeight - height, 0);
				auto lineHeight = mViewController.GetLineHeight();
				while (smallestPos > topOfLastScroll + 0.9 * lineHeight)
				{
					smallestPos -= lineHeight;
				}
				mScrollPosition = smallestPos;
			}
			::InvalidateRect(mWindow, nullptr, false);
			mIsRepeatedFind = false;
		}
	}
		break;
	case IDM_COPYWYSIWYG:
		BeginCopy(CopyMode::kWysiwyg, L"Wysywig");
		break;
	case IDM_COPYASHEXDIGITS:
		BeginCopy(CopyMode::kAsHexDigits, L"As hex digits");
		break;
	case IDM_COPYASCSTYLEASCII:
		BeginCopy(CopyMode::kAsCStyleAscii, L"As c-style ascii");
		break;
	case IDM_COPYUTF8:
		BeginCopy(CopyMode::kAsUtf8, L"As utf-8");
		break;
	case IDM_COPYUTF16LE:
		BeginCopy(CopyMode::kAsUtf16LE, L"As utf-16LE");
		break;
	case IDM_COPYUTF16BE:
		BeginCopy(CopyMode::kAsUtf16BE, L"As utf-16BE");
		break;
	case IDM_ABOUT:
		RegisterHyperlinkControlClass(mInstance);
		DialogBox(mInstance, MAKEINTRESOURCE(IDD_ABOUTBOX), mWindow, About);
		break;
	case IDM_ADVERTISEMENT:
		RegisterHyperlinkControlClass(mInstance);
		DialogBox(mInstance, MAKEINTRESOURCE(IDD_ADVERTISEMENT), mWindow, About);
		break;
	case IDM_EXIT:
		DestroyWindow(mWindow);
		break;
	default:
		result = false;
	}
	return result;
}

// Message handler for about box.
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
	UNREFERENCED_PARAMETER(lParam);
	switch (message)
	{
	case WM_INITDIALOG:
		return (INT_PTR)TRUE;

	case WM_COMMAND:
		if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
		{
			EndDialog(hDlg, LOWORD(wParam));
			return (INT_PTR)TRUE;
		}
		break;
	}
	return (INT_PTR)FALSE;
}


//--------------------------------

bool HexApplication::InitInstance(HINSTANCE inHInstance)
{
	mInstance = inHInstance;

	if (!::EnableMouseInPointer(true) || 
		!mViewController.CreateDeviceIndependentResources())
	{
		return false;
	}

	LoadString(mInstance, IDS_APP_TITLE, mWindowInitialTitle, kMaxLengthForLoadingString);
	LoadString(mInstance, IDC_HEXLOOK, mWindowClassName, kMaxLengthForLoadingString);
	
	WNDCLASSEX wcex;
	wcex.cbSize = sizeof(WNDCLASSEX);

	wcex.style = CS_HREDRAW | CS_VREDRAW;
	wcex.lpfnWndProc = WndProc;
	wcex.cbClsExtra = 0;
	wcex.cbWndExtra = 0;
	wcex.hInstance = mInstance;
	wcex.hIcon = LoadIcon(mInstance, MAKEINTRESOURCE(IDI_HEX));
	wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
	wcex.hbrBackground = 0;
	wcex.lpszMenuName = MAKEINTRESOURCE(IDC_HEXLOOK);
	wcex.lpszClassName = mWindowClassName;
	wcex.hIconSm = nullptr;

	bool result = false;
	if (ATOM atom = RegisterClassEx(&wcex))
	{
		mWindow = CreateWindow((LPCTSTR)atom, mWindowInitialTitle,
			WS_OVERLAPPEDWINDOW | WS_VSCROLL,
			CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, mInstance, this);
		ResetScrollBar();
		result = (nullptr != mWindow);
		if (result)
		{
			mFindTextDialog.SetInstance(mInstance);
			mFindTextDialog.SetParentWindow(mWindow);
			mCopier.SetMainWindow(mWindow);
			mCopyProgressWindow.InitWithParameters(mInstance, mWindow);
			mIsDelayedInvalidation = false;
		}
	}

	return result;
}

bool HexApplication::OpenFile(LPCTSTR inFilePath, LPCTSTR inDisplayName,
	bool inNeedStripQuotes)
{
	bool result = false;
	if (nullptr == inFilePath)
	{
		return false;
	}
	typedef std::basic_string<TCHAR> StringT;
	StringT name(inFilePath);
	if (inNeedStripQuotes && 2 < name.length() &&
		'\"' == name.front() && '\"' == name.back())
	{
		name = name.substr(1, name.length() - 2);
	}

	result = mFileHandler.OpenFile(name.c_str());
	if (result)
	{
		mViewController.ClearSelection();
		mIsRepeatedFind = false;
		AdjustWindowSize();
		ResetScrollBar();

		// append the file name to the initial window title
		StringT title(mWindowInitialTitle);
		if (inDisplayName)
		{
			name = inDisplayName;
		}
		else
		{
			size_t slashPosition = name.find_last_of(L"/\\");
			if (slashPosition != StringT::npos)
			{
				// erase up to slash, and the slash itself (+1)
				name.erase(0, slashPosition + 1);
			}
		}
		title = name + L" - " + title;
		::SetWindowText(mWindow, title.c_str());
	}

	return result;
}

void HexApplication::AdjustWindowSize()
{
	int maxLineWidth = mViewController.CalculateMaxWidth(mFileHandler);

	WINDOWINFO wi = { 0 };
	wi.cbSize = sizeof(wi);
	if (::GetWindowInfo(mWindow, &wi))
	{
		LONG oldWindowWidth = wi.rcWindow.right - wi.rcWindow.left;
		LONG oldClientWidth = wi.rcClient.right - wi.rcClient.left;

		// adding some margin at the right
		LONG newClientWidth = maxLineWidth + 5;
		int newWindowWidth = oldWindowWidth + (newClientWidth - oldClientWidth);
		int windowHeight = wi.rcWindow.bottom - wi.rcWindow.top;

		::SetWindowPos(mWindow, HWND_TOP, 0, 0, newWindowWidth, windowHeight,
			SWP_DRAWFRAME | SWP_NOCOPYBITS | SWP_NOMOVE | SWP_NOZORDER | SWP_NOREDRAW);
	}
}

void HexApplication::Show(int inCmdShow)
{
	ShowWindow(mWindow, inCmdShow);
	UpdateWindow(mWindow);
}

int HexApplication::Run()
{
	HACCEL hAccelTable = LoadAccelerators(mInstance, MAKEINTRESOURCE(IDC_HEXLOOK));

	HANDLE currentThread = ::OpenThread(SYNCHRONIZE, false,
		::GetCurrentThreadId());

	// Main message loop:
	MSG msg;
	while (GetMessage(&msg, NULL, 0, 0))
	{
		if (!mFindTextDialog.IsProcessedDlgMessage(&msg) &&
			!mCopyProgressWindow.IsProcessedDlgMessage(&msg) &&
			!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}

		// call the callback of async IO procedure when needed
		if (currentThread)
		{
			HANDLE handles[2] = { currentThread, 0 };
			while (!::PeekMessage(&msg, nullptr, 0, 0, 0))
			{
				int n = 1;
				if (HANDLE h = mFileHandler.GetEventForAsyncReading())
				{
					handles[1] = h;
					n = 2;
				}
				DWORD state = ::MsgWaitForMultipleObjectsEx(n, handles,
					INFINITE, QS_ALLEVENTS, MWMO_ALERTABLE);
				if (n > 1 && WAIT_OBJECT_0 + 1 == state)
				{
					mFileHandler.AcceptCurrentAsyncBlock();
					if (!mFileHandler.IsSearchFinished())
					{
						::PostMessage(mWindow, kWmMessageContinueFind, 0, 0);
					}
					else if (mCopier.IsCopying())
					{
						::PostMessage(mWindow, kWmMessageContinueCopy, 0, 0);
					}
					else if (mIsDelayedInvalidation)
					{
						::InvalidateRect(mWindow, nullptr, false);
						mIsDelayedInvalidation = false;
					}
				}
			}
		}
	}

	if (currentThread)
	{
		::CloseHandle(currentThread);
	}

	return (int)msg.wParam;
}

//-----------------------------
bool HexApplication::ProcessOpenCommand()
{
	bool result = false;
	IFileDialog *pfd = NULL;
	HRESULT hr = ::CoCreateInstance(CLSID_FileOpenDialog,
		NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pfd));

	if (SUCCEEDED(hr))
	{
		FILEOPENDIALOGOPTIONS flags;
		hr = pfd->GetOptions(&flags);
		if (!SUCCEEDED(hr))
		{
			flags = FOS_PATHMUSTEXIST | FOS_FILEMUSTEXIST;
		}
		hr = pfd->SetOptions(flags | FOS_FORCEFILESYSTEM | FOS_FORCESHOWHIDDEN);
		if (SUCCEEDED(hr))
		{
			hr = pfd->Show(NULL);
			if (SUCCEEDED(hr))
			{
				IShellItem *psiResult;
				hr = pfd->GetResult(&psiResult);
				if (SUCCEEDED(hr))
				{
					PWSTR path;
					hr = psiResult->GetDisplayName(SIGDN_FILESYSPATH, &path);
					if (SUCCEEDED(hr))
					{
						PWSTR displayName;
						hr = psiResult->GetDisplayName(SIGDN_NORMALDISPLAY, &displayName);
						if (!SUCCEEDED(hr))
						{
							displayName = nullptr;
						}

						if (OpenFile(path, displayName,  /*strip quotes*/false))
						{
							result = true;
							::InvalidateRect(mWindow, nullptr, false);
						}
						if (displayName)
						{
							::CoTaskMemFree(displayName);
						}
						::CoTaskMemFree(path);
					}
					psiResult->Release();
				}
			}
		}
		pfd->Release();
	}
	return result;
}

void HexApplication::ResetScrollBar()
{
	mScrollPosition = 0;
	mScaleForScroll = 1;
	mContentHeight = mViewController.GetContentHeight(mFileHandler);
	if (SHRT_MAX < mContentHeight)
	{
		mScaleForScroll = double(SHRT_MAX / mContentHeight);
	}
	double page = GetPageHeight(mViewController.GetLineHeight());


	SCROLLINFO si;
	si.cbSize = sizeof(si);
	si.fMask = SIF_DISABLENOSCROLL | SIF_RANGE | SIF_PAGE | SIF_POS;
	si.nMin = 0;
	si.nMax = int(::lround(mContentHeight * mScaleForScroll));
	si.nPage = int(::lround(page * mScaleForScroll));
	si.nPos = 0;

	int pos = ::SetScrollInfo(mWindow, SB_VERT, &si, /*redraw*/ true);
}

void HexApplication::OnScroll(int inScrollRequest, int inCurrentPosition)
{
	auto scrollPosition = mScrollPosition;
	double line = mViewController.GetLineHeight();
	double page = GetPageHeight(line);

	auto bottomPos = mContentHeight - page;
	bool shouldBecomeReadyBeforePaint = false;
	bool is = false;

	switch (inScrollRequest)
	{
	case SB_TOP:
		scrollPosition = 0;
		break;
	case SB_BOTTOM:
		scrollPosition = bottomPos;
		break;
	case SB_LINEUP:
		scrollPosition = max(0, scrollPosition - line);
		break;
	case SB_LINEDOWN:
		scrollPosition = min(scrollPosition + line, bottomPos);
		break;
	case SB_PAGEUP:
		scrollPosition = max(0, scrollPosition - page);
		break;
	case SB_PAGEDOWN:
		scrollPosition = min(scrollPosition + page, bottomPos);
		break;
	case SB_THUMBPOSITION:
		scrollPosition = inCurrentPosition / mScaleForScroll;
		shouldBecomeReadyBeforePaint = true;
		break;
	case SB_THUMBTRACK:
		scrollPosition = inCurrentPosition / mScaleForScroll;
		shouldBecomeReadyBeforePaint = true;
		break;
	case SB_ENDSCROLL:
		if (mCopier.IsCopying())
		{
			::PostMessage(mWindow, kWmMessageContinueCopy, 0, 0);
		}

		break;
	}

	if (scrollPosition > bottomPos)
	{
		scrollPosition = bottomPos;
	}

	if (SB_THUMBPOSITION != inScrollRequest && SB_THUMBTRACK != inScrollRequest)
	{
		// snap to integer multiple of the line height
		scrollPosition = lround(scrollPosition / line) * line;
	}
	ScrollTo(scrollPosition, shouldBecomeReadyBeforePaint);
}

void HexApplication::OnWheel(int inWheelDelta)
{
	double line = mViewController.GetLineHeight();

	UINT linesPerDelta = 3;
	if (!::SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &linesPerDelta, 0))
	{
		linesPerDelta = 3;
	}
	if (UINT_MAX == linesPerDelta)
	{
		double page = GetPageHeight(line);
		linesPerDelta = (UINT)max(1, lround(page / line));
	}

	auto deltaInUnits = static_cast<long double>(inWheelDelta) / WHEEL_DELTA;
	long double scrollPosition = mScrollPosition + 
		deltaInUnits * linesPerDelta * line;
	if (scrollPosition < 0)
	{
		scrollPosition = 0;
	}
	else
	{
		auto bottom = mContentHeight - GetPageHeight(line);
		if (scrollPosition > bottom)
		{
			scrollPosition = max(0, bottom);
		}
	}
	ScrollTo(scrollPosition, false);
}

void HexApplication::ScrollTo(long double inScrollPosition, bool inShouldBecomeReadyBeforePaint)
{
	if (inScrollPosition == mScrollPosition)
	{
		return;
	}
	auto address = mViewController.GetAddressForYPosition(inScrollPosition);
	bool isScrollingUp = (inScrollPosition > mScrollPosition);

	if (!mFileHandler.IsDataCachedAroundAddress(address, isScrollingUp))
	{
		if (mCopier.IsCopying() && mFileHandler.IsReadingAsync())
		{
			mFileHandler.AcceptCurrentAsyncBlock();
			::PostMessage(mWindow, kWmMessageContinueCopy, 0, 0);
		}
		mFileHandler.MakeReadInAdvance(address, isScrollingUp);
	}


	int scaledPos = int(inScrollPosition * mScaleForScroll);
	::SetScrollPos(mWindow, SB_VERT, scaledPos, /*redraw*/ true);

	RECT r = { 0 };
	::GetClientRect(mWindow, &r);

	int dy = lround(inScrollPosition - mScrollPosition);
	mScrollPosition = inScrollPosition;
	mIsDelayedInvalidation = false;

	if (inShouldBecomeReadyBeforePaint)
	{
		if (mFileHandler.IsReadingAsync() &&
			!mFileHandler.IsDataCachedAtAddress(address))
		{
			mIsDelayedInvalidation = true;
			::PostMessage(mWindow, kWmMessageCheckForReadFinish, 0, 0);
		}
	}
	if (!mIsDelayedInvalidation)
	{
		if (::abs(dy) < r.bottom / 2)
		{
			::InvalidateRect(mWindow, nullptr, false);
		}
		else
		{
			// update window
			::InvalidateRect(mWindow, nullptr, false);
		}
	}
}

double HexApplication::GetPageHeight(double inQuantum) const
{
	RECT clientRect;
	::GetClientRect(mWindow, &clientRect);
	double page = clientRect.bottom - clientRect.top;
	return ::floor(page / inQuantum) * inQuantum;
}

void HexApplication::OnSize(int inCx, int inCy)
{
	mViewController.ResizeRenderTarget(inCx, inCy);

	double line = mViewController.GetLineHeight();
	double page = (inCy / line) * line;

	SCROLLINFO si;
	si.cbSize = sizeof(si);
	si.fMask = SIF_DISABLENOSCROLL | SIF_PAGE;
	si.nPage = int(::lround(page * mScaleForScroll));

	int pos = ::SetScrollInfo(mWindow, SB_VERT, &si, /*redraw*/ true);

	RECT clientRect;
	if (::GetClientRect(mWindow, &clientRect))
	{
		if (inCx > clientRect.right)
		{
			RECT r = { clientRect.right, 0, inCx, inCy };
			::InvalidateRect(mWindow, &r, false);
		}
		if (inCy > clientRect.bottom)
		{
			int right = min(inCx, clientRect.right);
			RECT r = { 0, clientRect.bottom, right, inCy };
			::InvalidateRect(mWindow, &r, false);
		}
	}
}

POINT HexApplication::GetLocalMousePosition(LPARAM inLParam) const
{
	int x = GET_X_LPARAM(inLParam);
	int y = GET_Y_LPARAM(inLParam);

	WINDOWINFO wi;
	wi.cbSize = sizeof(wi);
	if (::GetWindowInfo(mWindow, &wi))
	{
		x -= wi.rcClient.left;
		y -= wi.rcClient.top;
	}
	return { x, y };
}

void HexApplication::InvalidateSelection()
{
	RECT rect;
	if (::GetClientRect(mWindow, &rect) && 
		mViewController.AdjustInvalidatedRect(LONGLONG(mScrollPosition), rect))
	{
		::InvalidateRect(mWindow, &rect, false);
		mViewController.ResetInvalidatedStrip();
	}
}

void HexApplication::BeginCopy(CopyMode inCopyMode, LPCTSTR inDescription)
{
	if (mViewController.IsSelectionPresent())
	{
		if (!mFileHandler.IsSearchFinished())
		{
			CancelFind();
		}

		LONGLONG sel1, sel2;
		mViewController.GetSelection(sel1, sel2);
		auto selectionBegin = min(sel1, sel2);
		auto selectionEnd = max(sel1, sel2);

		WCHAR description[100];
		::swprintf_s(description, L"%s %.4llx..%.4llx", inDescription,
			selectionBegin, selectionEnd);
		

		ProtectCachingForOnscreenData(true);

		mCopier.SetRange(selectionBegin, selectionEnd + 1);
		if (mCopier.PrepareStart(inCopyMode))
		{
			mCopyProgressWindow.ShowWithTimeLag(description);
			mCopier.Continue(mFileHandler);
			if (!mFileHandler.IsReadingAsync())
			{
				if (mCopier.IsDataReady())
				{
					mCopier.Finish(mWindow);
				}
				mCopyProgressWindow.HideWithTimeLag();
				ProtectCachingForOnscreenData(false);
			}
		}
	}
}

void HexApplication::ContinueCopying()
{
	mCopier.Continue(mFileHandler);
	mCopyProgressWindow.SetProgress(mCopier.GetPercentageDone());

	if (!mFileHandler.IsReadingAsync())
	{
		if (mCopier.IsDataReady())
		{
			mCopier.Finish(mWindow);
		}
		mCopyProgressWindow.HideWithTimeLag();
		ProtectCachingForOnscreenData(false);
	}
}

void HexApplication::CancelCopying()
{
	if (mCopier.IsCopying())
	{
		mFileHandler.StopReading();
		mCopier.Cancel();
		mCopyProgressWindow.Hide();
	}
}

void HexApplication::UpdateMenuItemStates(HMENU inSubMenu)
{
	int count = ::GetMenuItemCount(inSubMenu);
	if (count <= 0)
	{
		return;
	}
	static const std::array<UINT, 7> commandsEnabledWithNonEmptyFile =
	{
		IDM_GOTO_ADDRESS, IDM_FINDHEX, IDM_FINDUTF8, IDM_FINDUTF16LE,
		IDM_FINDUTF16BE, IDM_SELECTALL,	IDM_SELECTCUSTOM
	};
	static const std::array<UINT, 7> commandsEnabledWithSelection =
	{
		IDM_DESELECT, IDM_COPYWYSIWYG, IDM_COPYASHEXDIGITS,
		IDM_COPYASCSTYLEASCII, IDM_COPYUTF8, IDM_COPYUTF16LE, IDM_COPYUTF16BE
	};
	bool isFileNonEmpty = (0 < mFileHandler.GetFileLength());
	bool isSelectionPresent = mViewController.IsSelectionPresent();

	for (int i = 0; i < count; ++i)
	{
		UINT id = ::GetMenuItemID(inSubMenu, i);
		bool enable = true;
		bool needChange = true;
		auto iteratorSelection = std::find(commandsEnabledWithSelection.begin(),
			commandsEnabledWithSelection.end(), id);
		if (commandsEnabledWithSelection.end() != iteratorSelection)
		{
			enable = isSelectionPresent;
		}
		else
		{
			auto iteratorFile = std::find(commandsEnabledWithNonEmptyFile.begin(),
				commandsEnabledWithNonEmptyFile.end(), id);
			if (commandsEnabledWithNonEmptyFile.end() != iteratorFile)
			{
				enable = isFileNonEmpty;
			}
			else
			{
				needChange = false;
			}
		}
		if (needChange)
		{
			UINT flags = (MF_BYPOSITION | (enable ?
			MF_ENABLED : (MF_DISABLED | MF_GRAYED)));
			::EnableMenuItem(inSubMenu, i, flags);
		}
	}
}

void HexApplication::ProtectCachingForOnscreenData(bool inProtect)
{
	if (!inProtect)
	{
		mFileHandler.ProtectCachingForAddressRange(-1, -1);
		return;
	}
	RECT clientRect;
	if (::GetClientRect(mWindow, &clientRect))
	{
		auto addr = mViewController.GetAddressForYPosition(mScrollPosition);
		auto height = clientRect.bottom - clientRect.top;
		auto addr1 = mViewController.GetAddressForYPosition(mScrollPosition + height);
		mFileHandler.ProtectCachingForAddressRange(addr, int(addr1 - addr));
	}
}

void HexApplication::PerformFind(BOOL inDownwards)
{
	LONGLONG anchorAddress = 0;
	if (mViewController.IsSelectionPresent())
	{
		LONGLONG sel1, sel2;
		mViewController.GetSelection(sel1, sel2);
		if (inDownwards)
		{
			anchorAddress = min(sel1, sel2);
			if (mIsRepeatedFind)
			{
				++anchorAddress;
			}
		}
		else
		{
			anchorAddress = max(sel1, sel2);
			if (mIsRepeatedFind)
			{
				anchorAddress = max(1, anchorAddress) - 1;
			}
		}
	}
	else
	{
		anchorAddress = (inDownwards ? 0 : mFileHandler.GetFileLength() - 1);
	}

	mFindTextDialog.DisplayStatus(L"finding...");
	mFindTextDialog.EnableTheFindNextButton(false);
	ProtectCachingForOnscreenData(true);
	bool isFound = false;


	// tryout
	mFileHandler.StartFind(inDownwards, anchorAddress);
	if (mFileHandler.IsSearchFinished())
	{
		FinishFind();
	}
	else
	{
		if (!mFileHandler.LaunchAsyncReadingNextBlock())
		{
			::PostAppMessage(mWindow, kWmMessageContinueFind, 0, 0);
		}
	}
}

void HexApplication::FinishFind()
{
	LONGLONG rangeStart;
	int rangeLength;
	bool isMatchFound = mFileHandler.GetRangeOfFoundBytes(rangeStart, rangeLength);
	if (isMatchFound)
	{
		auto y = mViewController.GetYPositionForAddress(rangeStart);

		RECT clientRect;
		::GetClientRect(mWindow, &clientRect);
		int clientHeight = clientRect.bottom;
		auto lineHeight = mViewController.GetLineHeight();

		// scroll if the address is not visible on screen
		if (y + 1 <= mScrollPosition || y >= mScrollPosition + clientHeight)
		{
			auto topOfLastScroll = ::fmax(mContentHeight - clientHeight, 0);
			while (y > topOfLastScroll + 0.9 * lineHeight)
			{
				y -= lineHeight;
			}
			mScrollPosition = y;

			int scaledPos = int(mScrollPosition * mScaleForScroll);
			::SetScrollPos(mWindow, SB_VERT, scaledPos, /*redraw*/ true);
		}
		mViewController.SetSelection(rangeStart, rangeStart + rangeLength - 1);
		::InvalidateRect(mWindow, nullptr, false);
		mIsRepeatedFind = true;
	}
	ProtectCachingForOnscreenData(false);
	mFileHandler.SetSearchFinished();

	mFindTextDialog.DisplayStatus(isMatchFound ? L"done." : L"not found");
	mFindTextDialog.EnableTheFindNextButton(true);
}

void HexApplication::CancelFind()
{
	if (!mFileHandler.IsSearchFinished())
	{
		mFileHandler.StopReading();
		mFileHandler.SetSearchFinished();
		mFindTextDialog.DisplayStatus(L"the search is cancelled.");
		mFindTextDialog.EnableTheFindNextButton(true);
	}
}